Skip to main content
rustic_core uses AES-256-CTR encryption with Poly1305 authentication to ensure all data stored in repositories is secure and tamper-proof.

Encryption Overview

Every piece of data and metadata in a repository is encrypted before it leaves your machine. The storage backend never sees unencrypted data.

Security Properties

rustic_core’s encryption provides:

Confidentiality

All data encrypted with AES-256

Authenticity

Poly1305 MAC prevents tampering

Integrity

Corrupted data is detected

Forward Secrecy

Each blob uses unique nonce

Encryption Algorithm

rustic_core uses the AES256-CTR-Poly1305-AES construction:
1

AES-256-CTR Encryption

Data is encrypted using AES-256 in Counter (CTR) mode:
  • AES-256: Strong symmetric encryption with 256-bit keys
  • CTR mode: Stream cipher mode allowing random access
  • Performance: Hardware-accelerated on modern CPUs
2

Poly1305-AES Authentication

Encrypted data is authenticated with Poly1305-AES MAC:
  • Poly1305: Fast one-time authentication code
  • AES component: Additional randomness for security
  • Result: 16-byte authentication tag
3

Combined AEAD

This combination provides Authenticated Encryption with Associated Data (AEAD):
  • Encryption and authentication in one step
  • Prevents both eavesdropping and tampering
This is the same encryption construction used by restic, ensuring full compatibility.

The Master Key

At the heart of repository encryption is a 64-byte master key:
pub struct Key(AeadKey);  // 64 bytes total

// Key structure:
// [0..32]   - AES-256 encryption key
// [32..48]  - Poly1305-AES 'k' parameter  
// [48..64]  - Poly1305-AES 'r' parameter

Key Generation

The master key is randomly generated using a cryptographically secure RNG:
use rustic_core::crypto::aespoly1305::Key;

let key = Key::new();  // Generates random 64-byte key

Key Components

The first 32 bytes are used as the AES-256 encryption key.
let (encrypt, k, r) = key.to_keys();
// encrypt: [u8; 32] - AES-256 key

Password-Based Key Derivation

The master key is never stored directly. Instead, it’s encrypted with a key derived from your password using scrypt.

Scrypt Parameters

scrypt is a memory-hard key derivation function designed to resist brute-force attacks:
pub struct KeyFile {
    pub kdf: String,        // "scrypt"
    pub n: u32,            // CPU/memory cost (2^N)
    pub r: u32,            // Block size  
    pub p: u32,            // Parallelization
    pub salt: Vec<u8>,     // Random 64-byte salt
    pub data: Vec<u8>,     // Encrypted master key
}
Default parameters (recommended):
  • N: 2^15 (32768) - Memory cost factor
  • r: 8 - Block size
  • p: 1 - Parallelization
scrypt with these parameters is intentionally slow (~100ms) to make password guessing expensive. Don’t reduce these values!

Key Derivation Process

1

Generate Salt

Create a random 64-byte salt:
let mut salt = vec![0; 64];
rng().fill_bytes(&mut salt);
2

Derive Key

Use scrypt to derive a 64-byte key from the password:
let params = Params::recommended();
let mut derived_key = [0; 64];
scrypt::scrypt(password, &salt, &params, &mut derived_key)?;
3

Encrypt Master Key

Encrypt the master key with the derived key:
let kdf_key = Key::from_slice(&derived_key);
let encrypted_data = kdf_key.encrypt_data(&master_key_json)?;

Multiple Passwords

A repository can have multiple key files, each with a different password:
// Add a new password
let new_key_id = repo.add_key("new-password", &key_opts)?;

// Each key file contains the same master key, encrypted differently
This allows:
  • Different passwords for different team members
  • Password rotation without re-encrypting data
  • Emergency access keys

Encryption in Practice

Encrypting Data

When saving data to the repository:
use rustic_core::crypto::CryptoKey;

// The encryption process
let encrypted = key.encrypt_data(&plaintext)?;

// Encrypted format:
// [0..16]   - Random nonce
// [16..N-16] - Ciphertext  
// [N-16..N] - Poly1305 MAC tag
Each encryption uses a unique random nonce (number used once):
let mut nonce = Nonce::default();  // 16 bytes
rng().fill_bytes(&mut nonce);

Decrypting Data

When reading from the repository:
// Extract nonce and verify MAC
let nonce = &encrypted[0..16];
let ciphertext = &encrypted[16..];

let plaintext = key.decrypt_data(&encrypted)?;
// Returns error if MAC verification fails
If the MAC check fails, the data has been corrupted or tampered with. rustic_core will reject it.

What Gets Encrypted?

rustic_core encrypts everything except key files themselves:
Always encrypted:
  • Config file
  • Snapshot metadata
  • Tree blobs (directory structure)
  • Data blobs (file contents)
  • Index files
impl RepoFile for ConfigFile {
    const ENCRYPTED: bool = true;
}

impl RepoFile for SnapshotFile {
    const ENCRYPTED: bool = true;
}

Content-Addressed Encryption

Encryption interacts with deduplication:
  1. Plaintext chunking: Files are split into chunks based on content
  2. Deterministic IDs: Each chunk’s ID is the SHA-256 hash of plaintext
  3. Encrypted storage: Chunks are encrypted before storage
  4. Deduplication preserved: Identical chunks have same ID despite different ciphertexts
// Same content always generates same ID
let chunk_id = hash(&plaintext_chunk);  // SHA-256

// But different encrypted output (different nonces)
let encrypted1 = key.encrypt_data(&chunk);
let encrypted2 = key.encrypt_data(&chunk);
assert_ne!(encrypted1, encrypted2);  // Different ciphertexts

// Deduplication still works via content addressing
assert_eq!(hash(&chunk), hash(&chunk));  // Same ID

Security Considerations

Your password is the weakest link. Use a strong, unique password:
  • Minimum: 16+ characters
  • Recommended: Random passphrase or password manager
  • Avoid: Dictionary words, personal information
scrypt makes brute-forcing expensive, but can’t protect weak passwords.
The master key exists in memory during operations:
// Key is stored in repository state
pub(crate) fn dbe(&self) -> &DecryptBackend<Key> {
    &self.status.open_status().dbe
}
Security measures:
  • Keys are never written to disk unencrypted
  • Use encrypted swap or disable swap for maximum security
  • Clear sensitive data from memory when done
While all data is encrypted, some metadata is visible:Encrypted:
  • File names and paths
  • File contents
  • Directory structure
Visible:
  • Repository structure (config, keys, snapshots exist)
  • Pack file sizes
  • Number of snapshots
  • Approximate repository size
This is inherent to content-addressed storage.

Working with Keys

Opening with Password

use rustic_core::repository::Credentials;

let credentials = Credentials::Password("my-password".to_string());
let repo = repo.open(&credentials)?;

Opening with Master Key

For automation, you can use the master key directly:
use rustic_core::repofile::MasterKey;

// Export master key once
let master_key = repo.key();
let master_key_json = serde_json::to_string(&master_key)?;

// Later, open without password
let credentials = Credentials::Masterkey(master_key);
let repo = repo.open(&credentials)?;
Master keys provide full access without password. Store them securely (e.g., encrypted secrets management).

Key Management

// List keys
let keys: Vec<KeyId> = repo.list()?;

// Add new key
let key_id = repo.add_key("new-password", &key_opts)?;

// Delete key (cannot delete currently used key)
repo.delete_key(&key_id)?;

See Also

Repository

Repository structure and lifecycle

Deduplication

How encryption interacts with deduplication

Snapshots

What snapshot metadata is encrypted

Backends

Where encrypted data is stored